feat(cli): sync policies, diff/sync, and unified agent skill#752
Conversation
Add configurable source-of-truth rules in shelve.json (push/pull guards, onPushConflict, pull merge mode) with shelve diff and shelve sync. Enforce protected environments server-side via project syncPolicy. Merge shelve-app into a single comprehensive shelve agent skill.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Thank you for following the naming conventions! 🙏 |
|
Warning Review limit reached
More reviews will be available in 46 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughSync policies enable fine-grained control over push/pull conflict resolution between local and Shelve-hosted environments, including source-of-truth selection, conflict modes (overwrite/skip/fail/prompt), protected environments on the server, and pull-mode configuration (replace/merge). The implementation spans type definitions, database persistence, server authorization, CLI utilities, service orchestration, and three new/updated commands (diff, push, pull, sync). ChangesSync Policies Feature
Sequence DiagramssequenceDiagram
participant User
participant diff as shelve diff
participant sync as shelve sync
participant SyncSvc as SyncService
participant EnvSvc as EnvService
participant Shelve as Shelve API
User->>diff: shelve diff --env prod
diff->>SyncSvc: loadSyncContext()
SyncSvc->>EnvSvc: Load local .env file
SyncSvc->>Shelve: Fetch remote variables
SyncSvc->>SyncSvc: diffEnvVars() → onlyLocal, onlyRemote, changed, unchanged
SyncSvc-->>diff: SyncContext with diff result
diff-->>User: Display local vs remote (read-only)
User->>sync: shelve sync --env prod
sync->>SyncSvc: loadSyncContext()
SyncSvc-->>sync: SyncContext with sourceOfTruth policy
alt policy.sourceOfTruth === 'local'
sync->>SyncSvc: preparePushVariables(policy.onPushConflict)
SyncSvc-->>sync: variables + skipped/conflict keys
sync->>EnvSvc: pushEnvFile(variables, syncPolicy)
EnvSvc->>Shelve: Push variables
Shelve-->>EnvSvc: success/protected-env blocks
EnvSvc-->>sync: PushEnvFileResult
sync-->>User: Summary with pushed count, skipped/conflict keys
else policy.sourceOfTruth === 'remote'
sync->>SyncSvc: mergeForPull(policy.pullMode)
SyncSvc-->>sync: merged variables
sync->>EnvSvc: createEnvFile(variables, pullMode)
EnvSvc-->>sync: written file path
sync-->>User: Summary with variable count, keys written
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
commit: |
There was a problem hiding this comment.
Actionable comments posted: 13
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/lp/content/docs/3.cli/11.troubleshooting.md (1)
123-123:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUse singular wording for the unified catalog entry.
“Install the published skills” conflicts with the new single-skill model (
shelve). Rename to “Install the published skill” for consistency.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/lp/content/docs/3.cli/11.troubleshooting.md` at line 123, Change the heading/text "Install the published skills:" to the singular form "Install the published skill:" to match the new single-skill model; locate the phrase in the document (appearing as the installation instruction for the unified catalog) and update the wording accordingly so the catalog entry uses singular "skill" instead of plural "skills."
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/lp/skills/shelve/cli-commands.md`:
- Line 141: The example JSON in cli-commands.md is inconsistent: it shows
"action": "pull" but uses a contradictory "pushed": true field; update the
example so the output reflects a pull operation (e.g., remove or replace
"pushed" with a neutral field like "direction": "pull" or use a pull-specific
flag) — locate the JSON example containing "action": "pull" and "pushed" and
change "pushed": true to a neutral or pull-accurate value (for example
"direction": "pull" or "pushed": false) so the example is semantically
consistent.
In `@apps/shelve/app/pages/`[teamSlug]/projects/[projectId]/index/settings.vue:
- Around line 16-28: The computed protectedEnvironmentsText currently normalizes
the value on every input (get + dead set) causing commas to disappear
mid-typing; replace it with a plain ref (e.g., protectedEnvironmentsTextInput)
that you initialize from
project.value?.syncPolicy?.protectedEnvironments.join(', ') and update directly
on input so the raw string (including trailing comma/whitespace) remains
displayed; remove the empty set on the computed; then call the existing parsing
logic (reuse onProtectedEnvironmentsInput or move its parsing code) from an
onBlur handler to parse the ref into names and set
project.value.syncPolicy.protectedEnvironments (names.length ? names :
undefined); keep onProtectedEnvironmentsInput as the parsing/updating routine or
rename it and call it from blur, and ensure UInput binds to the new ref instead
of the computed.
In `@apps/shelve/server/api/teams/`[slug]/projects/[projectId]/index.put.ts:
- Around line 4-8: syncPolicySchema currently permits arbitrary nested values
(uses z.unknown()) which doesn't match the canonical types; replace it by
importing/using the shared Zod validators for ShelveSyncConfig and SyncPolicy
(or define a strict SyncPolicy schema) and use those instead of z.record(...
z.unknown()); ensure you reject unknown keys (use .strict()) and validate typed
fields like allowPush?: boolean, allowPull?: boolean, etc., then wire that
schema into the project update handler in place of syncPolicySchema so default
and environments are validated against the real SyncPolicy shape and
protectedEnvironments remains an array of non-empty strings.
In `@apps/shelve/server/utils/sync-policy.ts`:
- Around line 30-39: Replace the serial getEnvironmentName calls with a single
batched query: use an inArray-based fetch (import inArray) to load all
environments for the given teamId in one query (e.g. a new
helper/getEnvironmentsByIds or inline query that filters id
inArray(environmentIds) and teamId) then iterate the returned rows and call
assertPushAllowedForEnvironment(name, syncPolicy) for each environment; ensure
missing IDs are handled consistently with previous behavior and that inArray is
imported where used.
In `@packages/cli/schema.json`:
- Around line 64-71: The syncPolicy JSON schema properties (sourceOfTruth,
onPushConflict, pullMode, allowPush, allowPull, requireConfirmation) lack
description fields; update the schema to add a descriptive "description" for
each property explaining its purpose and valid values (e.g., sourceOfTruth:
explain "remote" vs "local"; onPushConflict: list options and behavior;
pullMode: describe "replace" vs "merge"; allowPush/allowPull: booleans control
push/pull capabilities; requireConfirmation: when user confirmation is required)
so IDEs can surface helpful tooltips and autocomplete guidance.
In `@packages/cli/src/commands/diff.ts`:
- Line 98: The final conditional is redundant because isJson() already causes an
early return earlier; remove the unnecessary if (!isJson()) check and call
cliSuccess(undefined, 'Diff complete') directly (or eliminate the call only if
that code path is unreachable by design) — update the code around the Diff
completion to use cliSuccess unconditionally instead of wrapping it in isJson()
check; reference functions: isJson() and cliSuccess.
- Around line 88-95: The lookup mismatch occurs because localMap and remoteMap
are created with uppercased keys but diff.changed contains original-cased keys
when autoUppercase is false; update the lookup so it uses the same casing rule
as map construction: either build maps using the original key casing from
syncContext.local/remote (remove toUpperCase when creating localMap/remoteMap)
or, if maps must be uppercased, change the lookup in the loop to use
key.toUpperCase() (i.e., for (const key of diff.changed) { lines.push(` ${key}:
${localMap.get(key.toUpperCase()) ?? '?'} → ${remoteMap.get(key.toUpperCase())
?? '?'}`) }); make the change in the block that constructs localMap/remoteMap
and in the loop that references diff.changed so both use identical casing logic.
In `@packages/cli/src/commands/push.ts`:
- Around line 89-90: The code mutates the result object returned from
pushEnvFile by adding skippedKeys and conflictKeys; instead modify pushEnvFile
to include skippedKeys and conflictKeys on its returned value (or have it return
a new structured object) and update the call site that currently assigns result
to use the new shape, or alternatively construct a new output object (e.g., {
...result, skippedKeys, conflictKeys }) instead of mutating result; locate
pushEnvFile and the variables result, skippedKeys, and conflictKeys to implement
the change and update any consumers accordingly.
In `@packages/cli/src/commands/sync.ts`:
- Around line 89-96: Extract the duplicated non-interactive confirmation check
into a shared helper (e.g., assertConfirmationAllowed or
ensureInteractiveOrConfirmed) and replace the inline block in sync.ts (the if
using confirmChanges, policy.requireConfirmation, skipConfirm,
isNonInteractive() that throws CliError) with a call to that helper; implement
the helper to accept the same flags/values (confirmChanges,
policy.requireConfirmation, skipConfirm) and throw the identical CliError with
the same message/code/suggestion when non-interactive confirmation is required,
then reuse that helper from push.ts and sync.ts to remove duplication and ensure
consistent behavior.
In `@packages/cli/src/services/sync.ts`:
- Around line 68-70: The current early return in the function in sync.ts hides
overwrite details by returning conflictKeys: [] when policy.onPushConflict ===
'overwrite'; change the logic so that you only return conflictKeys: [] when
conflictKeys.length === 0, but if conflicts exist and policy.onPushConflict ===
'overwrite' return the actual conflictKeys (alongside variables and skippedKeys)
so callers can detect which keys were overwritten; update the return at the
branch referencing variables, skippedKeys and conflictKeys accordingly.
- Around line 38-56: loadSyncContext currently takes five positional parameters
(project, environmentId, environmentName, slug, autoUppercase) which exceeds the
max-params lint rule; change its signature to accept a single options object
(e.g. { project, environmentId, environmentName, slug, autoUppercase }) and
update internal references to use options.project etc., keeping return type
SyncContext intact and still calling resolvePolicy(options.environmentName,
options.project, config.sync). Then update every call site that invokes
loadSyncContext in diff.ts, push.ts, pull.ts, and sync.ts to pass an object with
those named properties instead of positional arguments so the behavior remains
identical.
- Around line 130-138: confirmIfRequired currently discards the result of await
askBoolean(message) so a Ctrl+C cancel (the `@clack/prompts` cancel symbol) can
slip through; change confirmIfRequired to capture the response (const response =
await askBoolean(message)) and then if response is the cancel symbol (use
isCancel(response)) or response === false, call cliCancel() or throw to abort
the sync; update references to askBoolean, confirmIfRequired, isCancel, and
cliCancel accordingly so cancellation always stops the flow.
In `@packages/cli/test/sync-policy.test.ts`:
- Around line 10-32: Add tests to cover pullMode: 'merge' and allowPull blocking
in the resolveSyncPolicy suite: create one test calling
resolveSyncPolicy('development', { default: { pullMode: 'merge' } }) and assert
policy.pullMode === 'merge', and another test calling
resolveSyncPolicy('development', { default: { allowPull: false } }) and assert
policy.allowPull === false; place these alongside the existing tests in the
describe('resolveSyncPolicy') block so behavior for default overrides and
blocking is fully covered.
---
Outside diff comments:
In `@apps/lp/content/docs/3.cli/11.troubleshooting.md`:
- Line 123: Change the heading/text "Install the published skills:" to the
singular form "Install the published skill:" to match the new single-skill
model; locate the phrase in the document (appearing as the installation
instruction for the unified catalog) and update the wording accordingly so the
catalog entry uses singular "skill" instead of plural "skills."
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: fad56517-c7c2-4c91-8f5b-12601de87948
📒 Files selected for processing (43)
.changeset/sync-policies-cli.mdAGENTS.mdapps/lp/content/docs/3.cli/10.agents-automation.mdapps/lp/content/docs/3.cli/11.troubleshooting.mdapps/lp/content/docs/3.cli/12.sync-policies.mdapps/lp/content/docs/3.cli/2.index.mdapps/lp/content/docs/3.cli/5.push-pull.mdapps/lp/content/docs/3.cli/7.config.mdapps/lp/nuxt.config.tsapps/lp/skills/shelve/SKILL.mdapps/lp/skills/shelve/agent-workflows.mdapps/lp/skills/shelve/cli-commands.mdapps/lp/skills/shelve/platform.mdapps/lp/skills/shelve/sync-policies.mdapps/shelve/app/pages/[teamSlug]/projects/[projectId]/index/settings.vueapps/shelve/app/utils/zod/project.tsapps/shelve/server/api/teams/[slug]/projects/[projectId]/index.put.tsapps/shelve/server/api/teams/[slug]/projects/[projectId]/variables/[variableId]/index.put.tsapps/shelve/server/api/teams/[slug]/projects/[projectId]/variables/index.post.tsapps/shelve/server/db/migrations/postgresql/0007_project_sync_policy.sqlapps/shelve/server/db/migrations/postgresql/meta/_journal.jsonapps/shelve/server/db/schema.tsapps/shelve/server/utils/sync-policy.tsdocs/agents/cli.mddocs/agents/docs-links.mdpackages/cli/schema.jsonpackages/cli/src/commands/diff.tspackages/cli/src/commands/pull.tspackages/cli/src/commands/push.tspackages/cli/src/commands/sync.tspackages/cli/src/index.tspackages/cli/src/services/env.tspackages/cli/src/services/index.tspackages/cli/src/services/sync.tspackages/cli/src/utils/error-codes.tspackages/cli/src/utils/index.tspackages/cli/src/utils/sync-policy.tspackages/cli/test/sync-policy.test.tspackages/types/index.tspackages/types/schema.jsonpackages/types/src/Cli.tspackages/types/src/Project.tspackages/types/src/Sync.ts
Use camelCase "syncPolicy" in migration to match Drizzle schema (fixes e2e CI failures). Harden CLI push/sync/diff, server sync-policy batch checks, Zod validation, settings UI, and agent skill JSON examples.
There was a problem hiding this comment.
♻️ Duplicate comments (1)
packages/cli/src/services/sync.ts (1)
135-143:⚠️ Potential issue | 🟠 Major | ⚡ Quick winHandle cancellation in
confirmIfRequired.The result of
askBoolean(message)is discarded. If the user presses Ctrl+C,@clack/promptsreturns a cancel symbol thataskBooleanmay not handle internally, allowing execution to continue when it should abort.🐛 Proposed fix
static async confirmIfRequired( policy: ResolvedSyncPolicy, confirmChanges: boolean, skipConfirm: boolean, message: string, ): Promise<void> { const needsConfirm = (confirmChanges || policy.requireConfirmation) && !skipConfirm - if (needsConfirm) await askBoolean(message) + if (needsConfirm) { + const response = await askBoolean(message) + if (!response) cliCancel('Cancelled.') + } }Run the following script to verify how
askBooleanhandles cancellation:#!/bin/bash # Check askBoolean implementation to see if it handles cancel internally rg -nA 20 'export.*function askBoolean|export async function askBoolean' packages/cli/src🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/cli/src/services/sync.ts` around lines 135 - 143, confirmIfRequired currently discards askBoolean(message) result so a Ctrl+C cancel can slip through; update confirmIfRequired to await and capture the result (e.g. const result = await askBoolean(message)), then detect cancellation using the cancel symbol helper from `@clack/prompts` (or compare against the cancel constant) and throw/reject (or call process.exit(1)) to abort when cancelled; otherwise continue only when the boolean result indicates confirmation. Reference the confirmIfRequired function and the askBoolean call when making this change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Duplicate comments:
In `@packages/cli/src/services/sync.ts`:
- Around line 135-143: confirmIfRequired currently discards askBoolean(message)
result so a Ctrl+C cancel can slip through; update confirmIfRequired to await
and capture the result (e.g. const result = await askBoolean(message)), then
detect cancellation using the cancel symbol helper from `@clack/prompts` (or
compare against the cancel constant) and throw/reject (or call process.exit(1))
to abort when cancelled; otherwise continue only when the boolean result
indicates confirmation. Reference the confirmIfRequired function and the
askBoolean call when making this change.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: ead843db-d9fb-41f1-8e72-c85d3be15f98
📒 Files selected for processing (16)
apps/lp/skills/shelve/cli-commands.mdapps/shelve/app/pages/[teamSlug]/projects/[projectId]/index/settings.vueapps/shelve/app/utils/zod/project.tsapps/shelve/app/utils/zod/sync-policy.tsapps/shelve/server/api/teams/[slug]/projects/[projectId]/index.put.tsapps/shelve/server/db/migrations/postgresql/0007_project_sync_policy.sqlapps/shelve/server/utils/sync-policy.tspackages/cli/schema.jsonpackages/cli/src/commands/diff.tspackages/cli/src/commands/pull.tspackages/cli/src/commands/push.tspackages/cli/src/commands/sync.tspackages/cli/src/services/sync.tspackages/cli/src/utils/confirmation.tspackages/cli/src/utils/index.tspackages/cli/test/sync-policy.test.ts
Refactor confirmation logic to streamline user prompts. The confirmation check now returns early if confirmation is not needed, and the cancellation handling is improved to ensure consistent user experience.
…' into feat/sync-policies-unified-skill
Summary
shelve.json(sourceOfTruth,onPushConflict,pullMode,allowPush/allowPull,protectedEnvironments) with shared resolution in@typesand CLI enforcement onpush/pull.shelve diff(compare local vs Shelve, no writes) andshelve sync(apply policy;--dry-runsupported).syncPolicyon projects (migration0007), API push guard (ENV_PROTECTED), and protected environments in project Settings.shelve: merge formershelve-appintoplatform.md+sync-policies.md; remove duplicate published skill.Test plan
pnpm exec vitest runinpackages/clipnpm typecheck(CLI + app as needed)shelve diff --env developmentagainst a configured projectshelve pushblocked whenprotectedEnvironmentsincludes target envpnpm build:lpand verify/.well-known/skills/index.jsonlists onlyshelvenpx skills add https://shelve.cloud(or preview URL) installs one skillSummary by CodeRabbit
New Features
shelve diff(with JSON output) andshelve sync(policy-driven)shelve push/pullnow honor sync policies, pull-mode (merge/replace), and show skipped/conflict keysUI
Documentation